package com.stone.boot.pipeline.driver;

import com.stone.boot.pipeline.domain.bo.PipelineExecParam;
import com.stone.boot.pipeline.domain.vo.PipelineNodeVO;
import com.stone.boot.pipeline.domain.vo.PipelineVO;
import com.stone.boot.pipeline.executor.PipelineNodeExecutor;
import com.stone.boot.pipeline.infra.constant.StonePipelineConstant;
import com.stone.boot.pipeline.infra.exception.PipelineRuntimeException;
import com.stone.starter.core.helper.ApplicationContextHelper;
import com.stone.starter.core.helper.FastJsonHelper;
import com.stone.starter.redis.client.RedisCacheClient;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;

import java.util.Map;
import java.util.Set;

/**
 * 流水线驱动
 *
 * @author Mr_wenpan@163.com 2021/8/4 5:28 下午
 */
public class PipelineDriver {

    /**
     * 流程器开始方法
     *
     * @param pipelineCode       流水线编码
     * @param pipelineNodeParams 流程节点参数
     * @author Mr_wenpan@163.com 2021/8/4 5:44 下午
     */
    public static <T extends PipelineExecParam> void start(String pipelineCode, T pipelineNodeParams) {
        PipelineVO pipeline = getPipelineDetail(pipelineCode);
        start(pipeline, pipelineNodeParams);
    }

    /**
     * 流程器启动核心方法
     *
     * @param pipeline          流水线对象
     * @param pipelineExecParam 流程节点参数
     * @author Mr_wenpan@163.com 2021/8/4 5:45 下午
     */
    public static <T extends PipelineExecParam> void start(PipelineVO pipeline, T pipelineExecParam) {
        String pipelineCode = pipeline.getPipelineCode();
        if (pipelineExecParam == null) {
            throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage.PIPELINE_EXEC_PARAM_NULL, pipelineCode);
        }
        ApplicationContext applicationContext = ApplicationContextHelper.getContext();

        // 获取开始节点
        String runningAction = pipeline.getStartAction();
        // ========================================================================================================
        // 1、先获取到流程的开始节点的名称（即节点在容器中对应的bean的名称）
        // 2、通过bean的名称 + 类型获取将要执行的bean节点，并且执行该节点的run方法
        // 3、run方法执行完毕后，根据该方法设置的下一个策略，检查该节点的下一个策略是否是END，如果是，则跳出循环结束流程
        // 4、否则，将 【当前节点的bean名称 + , + 下一个执行策略（SUCCESS/FAILED）】进行拼接成唯一key，去存放所有
        // 流程节点的map集合中获取到当前节点信息。
        //     4.1、如果获取到的节点为空，则说明对当前节点的xxx执行策略没有对应的后续节点处理，则跳出循环结束流程
        //     4.2、如果获取到的节点的下一个节点为空，则说明对当前节点的xxx执行策略没有对应的后续节点处理，则跳出循环结束流程
        // 5、根据第4步获取到的节点，获取该节点的下一个将要执行的节点（即bean的名称）
        // 6、继续循环上面 2--5 步，直到跳出循环
        // 7、跳出循环后再次检查是否有配置结束流程，若有则执行结束流程的节点
        // ========================================================================================================
        while (true) {
            // 通过bean的名称 + 类型获取将要执行的bean
            PipelineNodeExecutor<T> pipelineNodeExecutor = applicationContext.getBean(runningAction, PipelineNodeExecutor.class);
            // 执行节点对应的run方法
            pipelineNodeExecutor.run(pipelineExecParam);
            // 如果下一个决策类型是END则跳出循环结束流程
            if (StonePipelineConstant.PipelineStrategy.END.equalsIgnoreCase(pipelineExecParam.getNextStrategy())) {
                break;
            }
            String runningNodeId = PipelineNodeVO.uniqueKey(runningAction, pipelineExecParam.getNextStrategy());
            // 通过将当前节点拼接当前节点策略类型去存放所有节点的map中取到当前节点对应的失败或成功处理策略
            PipelineNodeVO curPipelineNode = pipeline.getPipelineNodes().get(runningNodeId);
            // 如果没有取到或下个节点为空，则跳出循环结束流程
            if (curPipelineNode == null || StringUtils.isBlank(curPipelineNode.getNextAction())) {
                break;
            }
            // 当前节点重新赋值为当前节点的下一个节点继续执行循环
            runningAction = curPipelineNode.getNextAction();
        }

        // 执行流程结束节点
        if (StringUtils.isNotBlank(pipeline.getEndAction())) {
            PipelineNodeExecutor<T> pipelineNodeExecutor = applicationContext.getBean(pipeline.getEndAction(), PipelineNodeExecutor.class);
            pipelineNodeExecutor.run(pipelineExecParam);
        }
    }

    /**
     * 根据流程器编码构建流程器详情
     *
     * @param pipelineCode 流程编码
     * @return com.stone.boot.pipeline.vo.PipelineVO
     * @author Mr_wenpan@163.com 2021/8/4 5:46 下午
     */
    public static PipelineVO getPipelineDetail(String pipelineCode) {
        // ========================================================================================================
        // 1、首先检查流程编码，防止coder没有传递流程编码
        // 2、通过流程编码去Redis获取整个流程的信息（Redis中存储的是用hash类型存储的），并转换为PipelineVO对象
        // 3、如果没有花去到流程信息，或者整个流程里没有任何节点，则这是一个错误的流程
        // 4、校验流程中是否有维护开始节点，若没有，则这是一个错误的流程，流程无法运行
        // 5、遍历流水线所有的节点，检查每个节点的正确性，具体检查项如下：
        //     5.1、如果该节点是整个流程的开始节点，则暂时记录下来（留到最后检查是否有找到开始节点）
        //     5.2、获取当前节点的下一个节点进行检查
        //          5.2.1、如果下一个节点是空的，则直接跳过
        //          5.2.2、如果下个节点不存在于该流程的所有节点中，则这是一个异常流程
        //          5.2.3、如果当前节点的下一个节点是自己（死循环），抛出异常
        // 6、如果遍历完所有要执行的节点但是仍然没有找到流程的开始节点，说明整个流程有问题
        // ========================================================================================================
        if (StringUtils.isBlank(pipelineCode)) {
            throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage.PIPELINE_CODE_NULL);
        }

        // 从Redis中获取整个流程信息并转换为PipelineVO对象
        String pipelineKey = String.format(StonePipelineConstant.Redis.PIPELINE_KEY, pipelineCode);
        RedisCacheClient redisCacheClient = ApplicationContextHelper.getContext().getBean(RedisCacheClient.class);
        String pipelineInfo = redisCacheClient.<String, String>boundHashOps(pipelineKey).get(StonePipelineConstant.Redis.PIPELINE_NODE_INFO);
        PipelineVO pipeline = FastJsonHelper.stringToObject(pipelineInfo, PipelineVO.class);

        // 如果该流程没有节点，则这是一个异常的流程
        if (pipeline == null || MapUtils.isEmpty(pipeline.getPipelineNodes())) {
            throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage.PIPELINE_NULL, pipelineCode);
        }

        // 如果该流程没有开始节点，则这是一个异常的流程
        if (StringUtils.isBlank(pipeline.getStartAction())) {
            throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage.PIPELINE_START_ACTION_NULL, pipelineCode);
        }

        PipelineNodeVO startNode = null;
        Set<String> allNodeAction = pipeline.getAllNodeAction();
        // 遍历这个流水线编码对应的所有流程节点（检查流水线的正确性）
        for (Map.Entry<String, PipelineNodeVO> var : pipeline.getPipelineNodes().entrySet()) {
            PipelineNodeVO pipelineNode = var.getValue();
            // 如果该节点是整个流程的开始节点，则暂时记录下来
            if (pipeline.getStartAction().equalsIgnoreCase(pipelineNode.getCurAction())) {
                startNode = pipelineNode;
            }

            // 获取当前节点的下一个节点进行检查
            String nextAction = pipelineNode.getNextAction();
            // 如果下一个节点是空的，则直接跳过
            if (StringUtils.isBlank(nextAction)) {
                continue;
            }
            // 如果下个节点不存在于该流程的所有节点中，则这是一个异常流程
            if (!allNodeAction.contains(nextAction)) {
                throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage.PIPELINE_NODE_OUT_RANGE,
                        pipelineCode, pipelineNode.getNodeId(), pipelineNode.getCurAction(), nextAction);
            }
            // 如果当前节点的下一个节点是自己（死循环）
            if (nextAction.equalsIgnoreCase(pipelineNode.getCurAction())) {
                throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage.PIPELINE_NODE_CLOSED_LOOP,
                        pipelineCode, pipelineNode.getNodeId(), pipelineNode.getCurAction(), nextAction);
            }
        }
        // 如果遍历完所有要执行的节点但是仍然没有找到流程的开始节点，说明整个流程有问题
        if (startNode == null) {
            throw new PipelineRuntimeException(StonePipelineConstant.ErrorMessage
                    .PIPELINE_START_NODE_NOT_MATCH, pipelineCode, pipeline.getStartAction());
        }

        return pipeline;
    }
}
